﻿using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using FluentNHibernate.Cfg;
using System.Reflection;
using FluentNHibernate.Automapping;
using NHibernate.Tool.hbm2ddl;

namespace Fx.Data.NHibernate
{
    public static class SessionConfigurer
    {

        /// <summary>
        /// The default factory key used if only one database is being communicated with.
        /// </summary>
        public static readonly string DefaultFactoryKey = "nhibernate.current_session";

        private static IInterceptor RegisteredInterceptor;

        static SessionConfigurer()
        {
            SessionFactories = new Dictionary<string, ISessionFactory>();
            Storages = new Dictionary<string, ISessionStorage>();
        }

        #region Public methods overloads

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies)
        {
            return Init(storage, mappingAssemblies, null, null, null);
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, string cfgFile)
        {
            return Init(storage, mappingAssemblies, cfgFile, null, null);
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, IDictionary<string, string> cfgProperties)
        {
            return Init(storage, mappingAssemblies, null, cfgProperties, null);
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, string cfgFile, string validatorCfgFile)
        {
            return Init(storage, mappingAssemblies, cfgFile, null, validatorCfgFile);
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, string cfgFile, IDictionary<string, string> cfgProperties, string validatorCfgFile)
        {
            var cfg = ConfigureNHibernate(cfgFile, cfgProperties);
            var sessionFactory = CreateSessionFactory(mappingAssemblies, cfg);
            SessionFactories.Add(storage.FactoryKey, sessionFactory);
            Storages.Add(storage.FactoryKey, storage);
            return cfg;
        }

        /// <summary>
        /// Used to get the current NHibernate session associated with a factory key; i.e., the key 
        /// associated with an NHibernate session factory for a specific database.
        /// 
        /// If you're only communicating with one database, you should call <see cref="Current" /> instead,
        /// although you're certainly welcome to call this if you have the factory key available.
        /// </summary>
        public static ISession GetSessionBy(string factoryKey)
        {
            ISession session = Storages[factoryKey].Session;

            if (session == null)
            {
                if (RegisteredInterceptor != null)
                {
                    session = SessionFactories[factoryKey].OpenSession(RegisteredInterceptor);
                }
                else
                {
                    session = SessionFactories[factoryKey].OpenSession();
                }

                Storages[factoryKey].Session = session;
            }

            return session;
        }

        public static void RegisterInterceptor(IInterceptor interceptor)
        {
            RegisteredInterceptor = interceptor;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Provides access to the <see cref="ISessionFactory" /> assuming you're communicating
        /// with one database; otherwise, you should use <see cref="SessionFactories" />.
        /// </summary>
        public static ISessionFactory SessionFactory
        {
            get
            {
                return SessionFactories[DefaultFactoryKey];
            }
            set
            {
                if (value != null)
                {
                    if (SessionFactories.Count == 0)
                    {
                        SessionFactories.Add(DefaultFactoryKey, value);
                    }
                    // Replace existing session factory if different
                    else if (!value.Equals(SessionFactories.First()))
                    {
                        RemoveOneAndOnlySessionFactory();
                        SessionFactories.Add(DefaultFactoryKey, value);
                    }
                }
                else
                {
                    if (SessionFactories.Count > 0)
                    {
                        RemoveOneAndOnlySessionFactory();
                    }
                }
            }
        }

        public static ISessionStorage Storage
        {
            get
            {
                return Storages.Values.FirstOrDefault();
            }
            set
            {
                if (value != null)
                {
                    if (Storages.Count == 0)
                    {
                        Storages.Add(DefaultFactoryKey, value);
                    }
                    // Replace existing session storage if different
                    else if (!value.Equals(Storages.First()))
                    {
                        RemoveOneAndOnlySessionStorage();
                        Storages.Add(DefaultFactoryKey, value);
                    }
                }
                else
                {
                    if (Storages.Count > 0)
                    {
                        RemoveOneAndOnlySessionStorage();
                    }
                }
            }
        }

        public static bool UseMultipleDatabases
        {
            get
            {
                return SessionFactories.Count > 1;
            }
        }

        /// Used to get the current NHibernate session if you're communicating with a single database.
        /// When communicating with multiple databases, invoke <see cref="GetSessionBy()" /> instead.
        /// </summary>
        public static ISession Current
        {
            get
            {
                return GetSessionBy(DefaultFactoryKey);
            }
        }

        /// <summary>
        /// Maintains a dictionary of NHibernate session factories, one per database.  The key is 
        /// the "factory key" used to look up the associated database, and used to decorate respective
        /// repositories.  If only one database is being used, this dictionary contains a single
        /// factory with a key of <see cref="DefaultFactoryKey" />.
        /// </summary>
        public static IDictionary<string, ISessionFactory> SessionFactories { get; set; }

        /// <summary>
        /// Maintains a dictionary of NHibernate session storages, one per database.  The key is 
        /// the "factory key" used to look up the associated database, and used to decorate respective
        /// repositories.  If only one database is being used, this dictionary contains a single
        /// session storage with a key of <see cref="DefaultFactoryKey" />.
        /// </summary>
        public static IDictionary<string, ISessionStorage> Storages { get; set; }

        #endregion

        #region Private methods

        private static void RemoveOneAndOnlySessionFactory()
        {
            if (SessionFactories.Count > 0)
            {
                ISessionFactory factory = SessionFactories.Values.First();
                factory.Dispose();
                factory = null;
                SessionFactories.Clear();
            }
        }

        private static void RemoveOneAndOnlySessionStorage()
        {
            if (Storages.Count > 0)
            {
                ISessionStorage storage = Storages.Values.First();
                storage = null;
                Storages.Clear();
            }
        }

        private static string MakeLoadReadyAssemblyName(string assemblyName)
        {
            return (assemblyName.IndexOf(".dll") == -1)
                ? assemblyName.Trim() + ".dll"
                : assemblyName.Trim();
        }

        private static Configuration ConfigureNHibernate(string cfgFile, IDictionary<string, string> cfgProperties)
        {
            Configuration cfg = new Configuration();

            if (cfgProperties != null)
                cfg.AddProperties(cfgProperties);

            if (string.IsNullOrEmpty(cfgFile))
                return cfg.Configure();

            return cfg.Configure(cfgFile);
        }

        private static ISessionFactory CreateSessionFactory(string[] mappingAssemblies, Configuration cfg)
        {
            var fluentConfiguration = Fluently.Configure(cfg);

            fluentConfiguration.Mappings(m =>
            {
                foreach (var mappingAssembly in mappingAssemblies)
                {
                    var assembly = Assembly.LoadFrom(MakeLoadReadyAssemblyName(mappingAssembly));
                    m.HbmMappings.AddFromAssembly(assembly);
                    m.FluentMappings.AddFromAssembly(assembly);
                }
            });

            return fluentConfiguration
                .ExposeConfiguration(config => new SchemaExport(config).Create(true, true))
                .BuildSessionFactory();
        }

        #endregion

    }
}
